package org.softee.functional.functions;

import java.util.Collection;
import java.util.Comparator;

import org.softee.functional.helper.Preconditions;


public class ReduceFunctions {

    /**
     * Intentionally non-instantiable
     */
    private ReduceFunctions() {

    }

    /**
     * TODO Find out why type inference doesn't work when using max() directly
     */
    public static ReduceFunction<Integer> maxInteger() {
        return max();
    }

    /**
     * TODO Find out why type inference doesn't work when using min() directly
     */
    public static ReduceFunction<Integer> minInteger() {
        return min();
    }

    /**
     * @return a reduce function that finds the largest value (ignoring null values)
     */
    public static <T extends Comparable<? super T>> ReduceFunction<T> max() {
        return signum(+1);
    }

    /**
     * @return a reduce function that finds the largest value (ignoring null values)
     */
    public static <T> ReduceFunction<T> max(Comparator<? super T> comparator) {
        return signum(+1, comparator);
    }

    /**
     * @return a reduce function that finds the smallest value (ignoring null values)
     */
    public static <T extends Comparable<? super T>> ReduceFunction<T> min() {
        return signum(-1);
    }

    /**
     * @return a reduce function that finds the smallest value (ignoring null values)
     */
    public static <T> ReduceFunction<T> min(Comparator<? super T> comparator) {
        return signum(-1, comparator);
    }

    private static <T extends Comparable<? super T>> ReduceFunction<T> signum(final int plusMinusOne) {
        return new CompareReduceFunction<T>(plusMinusOne) {
            @Override
            public int compare(T input, T result) {
                return input.compareTo(result);
            }
        };
    }

    private static <T> ReduceFunction<T> signum(final int plusMinusOne, final Comparator<? super T> comparator) {
        Preconditions.notNull(comparator);
        return new CompareReduceFunction<T>(plusMinusOne) {
            @Override
            public int compare(T input, T result) {
                return comparator.compare(input, result);
            }
        };
    }
    public static ReduceFunction<Integer> integerSum() {
        return sum(new Summable<Integer>() {
            @Override
            public Integer sum(Integer addend1, Integer addend2) {
                return addend1 + addend2;
            }
        });
    }

    public static <T extends Number> ReduceFunction<T> sum(final Summable<T> summable) {
        return new ReduceFunction<T>() {
            @Override
            public T apply(Collection<T> inputs) {
                T result = null;
                for (T number : inputs) {
                    if (number != null) {
                        result = (result != null) ?  summable.sum(result, number) : number;
                    }
                }
                return result;
            }
        };
    }

    /**
     * FIXME size() will only work on an Integer collection. That's silly, so something have to be done about that.
     * But it isn't straightforward, since the semantics of reduce (Functional<T>.reduce()) returns T
     */
    public static ReduceFunction<Integer> size() {
        return new SequentialReduceFunction<Integer>() {
            @Override
            public Integer apply(Collection<Integer> input) {
                return input.size();
            }
        };
    }

    private interface SequentialReduceFunction<T> extends ReduceFunction<T>, SequentialOnly {

    }

    public interface Summable<T> {
        T sum(T addend1, T addend2);
    }

    private static abstract class CompareReduceFunction<T> implements ReduceFunction<T> {
        final int plusMinusOne;

        public CompareReduceFunction(int plusMinusOne) {
            this.plusMinusOne = plusMinusOne;
        }

        @Override
        public T apply(Collection<T> inputs) {
            T result = null;
            for (T input : inputs) {
                if (input != null) {
                    if (result != null) {
                        int compareTo = compare(input, result);
                        if (Integer.signum(compareTo) == plusMinusOne) {
                            result = input;
                        }
                    } else {
                        result = input;
                    }
                }
            }
            return result;
        }

        public abstract int compare(T input, T result);

    }
}
